优化自动化脚本:定义组件名与类型别名(加入前缀 Vp)
概述
为组件库的所有组件添加统一前缀 Vp(如 VpButton、VpMenu),使组件名具有品牌辨识度且避免与原生 HTML 元素冲突。同时优化自动化构建脚本,在生成组件入口文件时自动添加前缀,为后续 unplugin-vue-components 自动导入提供 Resolver 基础。
为什么需要组件前缀
| 问题 | 无前缀 | 有前缀 Vp |
|---|---|---|
| 命名冲突 | <Header> 与 HTML <header> 冲突 | <VpHeader> 无冲突 |
| 品牌辨识 | 无法区分组件来源 | 一看即知来自 Vp 组件库 |
| 自动导入规则 | 难以定义 Resolver 匹配规则 | Vp 开头即匹配 |
| IDE 提示 | 与原生标签混淆 | 独立的组件命名空间 |
业界通用实践:Element Plus(El)、Ant Design Vue(A)、Naive UI(N)。
自动化脚本优化
构建脚本:自动生成带前缀的组件入口
// scripts/build-components.ts
import glob from 'fast-glob'
import path from 'node:path'
import fs from 'node:fs'
const COMPONENT_PREFIX = 'Vp'
const COMPONENT_DIR = 'src/components'
interface ComponentMeta {
/** 原始组件名(如 Button) */
name: string
/** 带前缀的组件名(如 VpButton) */
prefixedName: string
/** 文件路径 */
filePath: string
}
/**
* 扫描组件目录,收集组件元信息
*/
async function scanComponents(): Promise<ComponentMeta[]> {
const files = await glob('**/index.vue', { cwd: COMPONENT_DIR })
return files.map(file => {
const dirName = path.dirname(file) // e.g., 'button'
const name = dirName
.split(/[-/]/)
.map(seg => seg.charAt(0).toUpperCase() + seg.slice(1))
.join('') // e.g., 'Button'
return {
name,
prefixedName: `${COMPONENT_PREFIX}${name}`, // e.g., 'VpButton'
filePath: path.join(COMPONENT_DIR, file)
}
})
}
/**
* 生成组件入口文件(带前缀导出)
*/
async function generateEntry(): Promise<void> {
const components = await scanComponents()
const imports = components.map(c =>
`import ${c.prefixedName} from './${path.dirname(c.filePath)}'`
).join('\n')
const exports = components.map(c =>
`export { ${c.prefixedName} }`
).join('\n')
const installList = components.map(c =>
` app.component('${c.prefixedName}', ${c.prefixedName})`
).join('\n')
const content = `// Auto-generated by build script. Do not edit manually.
import type { App } from 'vue'
${imports}
const components = [${components.map(c => c.prefixedName).join(', ')}]
export function install(app: App): void {
${installList}
}
export { ${components.map(c => c.prefixedName).join(', ')} }
export default { install }
`
fs.writeFileSync('src/index.ts', content)
console.log(`Generated entry with ${components.length} components`)
}
generateEntry()
typescript
生成类型声明
// scripts/build-types.ts
import glob from 'fast-glob'
import fs from 'node:fs'
import path from 'node:path'
const COMPONENT_PREFIX = 'Vp'
interface TypeAlias {
original: string
aliased: string
}
async function generateTypeDeclarations(): Promise<void> {
const files = await glob('**/index.vue', { cwd: 'src/components' })
const typeAliases: TypeAlias[] = files.map(file => {
const dirName = path.dirname(file)
const name = dirName
.split(/[-/]/)
.map(seg => seg.charAt(0).toUpperCase() + seg.slice(1))
.join('')
return {
original: name,
aliased: `${COMPONENT_PREFIX}${name}`
}
})
const content = `// Auto-generated type declarations
import type { DefineComponent } from 'vue'
${typeAliases.map(t =>
`export const ${t.aliased}: DefineComponent<{}, {}, any>`
).join('\n')}
declare module 'vue' {
export interface GlobalComponents {
${typeAliases.map(t =>
` ${t.aliased}: typeof import('@vp/components')['${t.aliased}']`
).join('\n')}
}
}
export {}
`
fs.writeFileSync('dist/types/components.d.ts', content)
}
generateTypeDeclarations()
typescript
自定义 Resolver 实现
unplugin-vue-components Resolver
// src/resolver.ts
import type { ComponentResolver } from 'unplugin-vue-components'
const COMPONENT_PREFIX = 'Vp'
const LIBRARY_NAME = '@vp/components'
/**
* Vp 组件库的自动导入 Resolver
* 使用方式:在模板中写 <VpButton>,自动从 @vp/components 导入
*/
export function VpResolver(): ComponentResolver {
return {
type: 'component',
resolve: (name: string) => {
// 匹配 Vp 前缀的组件名
if (name.startsWith(COMPONENT_PREFIX)) {
const partialName = name.slice(COMPONENT_PREFIX.length)
return {
name,
from: LIBRARY_NAME,
sideEffects: `${LIBRARY_NAME}/dist/styles/${partialName.toLowerCase()}.css`
}
}
}
}
}
typescript
在 Vite 中配置
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import Components from 'unplugin-vue-components/vite'
import { VpResolver } from '@vp/components/resolver'
export default defineConfig({
plugins: [
vue(),
Components({
resolvers: [
VpResolver(),
// 可同时配置其他组件库的 Resolver
// ElementPlusResolver()
],
dts: 'src/auto-components.d.ts' // 自动生成类型声明
})
]
})
typescript
组件名前缀对比表
| 组件原名 | 带前缀名 | 导入路径 |
|---|---|---|
Button | VpButton | @vp/components |
Menu | VpMenu | @vp/components |
MenuItem | VpMenuItem | @vp/components |
Table | VpTable | @vp/components |
Form | VpForm | @vp/components |
实践要点
- 组件前缀
Vp统一添加在自动化脚本中,避免手动修改每个组件文件 unplugin-vue-components的 Resolver 通过前缀匹配规则判断组件来源- 自动生成的入口文件包含
install方法(全局注册)和具名导出(按需导入) - 类型声明通过
declare module 'vue'扩展GlobalComponents接口,使 IDE 能识别组件类型 - 打包后验证全局注册名称正确,确保组件名与前缀一致后再发布
↑